package cs236369.webserver.examples;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.Properties;
import java.util.Vector;

class WebServerExample implements HttpConstants {

	/* static class data/methods */

	/* print to stdout */
	protected static void p(String s) {
		System.out.println(s);
	}

	/* print to the log file */
	protected static void log(String s) {
		synchronized (log) {
			log.println(s);
			log.flush();
		}
	}

	static PrintStream log = null;
	/*
	 * our server's configuration information is stored in these properties
	 */
	protected static Properties props = new Properties();

	/* Where worker threads stand idle */
	static Vector threads = new Vector();

	/* the web server's virtual root */
	static File root;

	/* timeout on client connections */
	static int timeout = 0;

	/* max # worker threads */
	static int workers = 5;

	/* load www-server.properties from java.home */
	static void loadProps() throws IOException {
		File f = new File(System.getProperty("java.home") + File.separator
				+ "lib" + File.separator + "www-server.properties");
		if (f.exists()) {
			InputStream is = new BufferedInputStream(new FileInputStream(f));
			props.load(is);
			is.close();
			String r = props.getProperty("root");
			if (r != null) {
				root = new File(r);
				if (!root.exists()) {
					throw new Error(root + " doesn't exist as server root");
				}
			}
			r = props.getProperty("timeout");
			if (r != null) {
				timeout = Integer.parseInt(r);
			}
			r = props.getProperty("workers");
			if (r != null) {
				workers = Integer.parseInt(r);
			}
			r = props.getProperty("log");
			if (r != null) {
				p("opening log file: " + r);
				log = new PrintStream(new BufferedOutputStream(
						new FileOutputStream(r)));
			}
		}

		/* if no properties were specified, choose defaults */
		if (root == null) {
			root = new File(System.getProperty("user.dir"));
		}
		if (timeout <= 1000) {
			timeout = 5000;
		}
		if (workers < 25) {
			workers = 5;
		}
		if (log == null) {
			p("logging to stdout");
			log = System.out;
		}
	}

	static void printProps() {
		p("root=" + root);
		p("timeout=" + timeout);
		p("workers=" + workers);
	}

	public static void main(String[] a) throws Exception {
		int port = 8080;
		if (a.length > 0) {
			port = Integer.parseInt(a[0]);
		}
		loadProps();
		printProps();
		/* start worker threads */
		for (int i = 0; i < workers; ++i) {
			Worker w = new Worker();
			(new Thread(w, "worker #" + i)).start();
			threads.addElement(w);
		}

		ServerSocket ss = new ServerSocket(port);
		while (true) {

			Socket s = ss.accept();

			Worker w = null;
			synchronized (threads) {
				if (threads.isEmpty()) {
					Worker ws = new Worker();
					ws.setSocket(s);
					(new Thread(ws, "additional worker")).start();
				} else {
					w = (Worker) threads.elementAt(0);
					threads.removeElementAt(0);
					w.setSocket(s);
				}
			}
		}
	}
}

class Worker extends WebServerExample implements HttpConstants, Runnable {
	final static int BUF_SIZE = 2048;

	static final byte[] EOL = { (byte) '\r', (byte) '\n' };

	/* buffer to use for requests */
	byte[] buf;
	/* Socket to client we're handling */
	private Socket s;

	Worker() {
		buf = new byte[BUF_SIZE];
		s = null;
	}

	synchronized void setSocket(Socket s) {
		this.s = s;
		notify();
	}

	public synchronized void run() {
		while (true) {
			if (s == null) {
				/* nothing to do */
				try {
					wait();
				} catch (InterruptedException e) {
					/* should not happen */
					continue;
				}
			}
			try {
				handleClient();
			} catch (Exception e) {
				e.printStackTrace();
			}
			/*
			 * go back in wait queue if there's fewer than numHandler
			 * connections.
			 */
			s = null;
			Vector pool = WebServerExample.threads;
			synchronized (pool) {
				if (pool.size() >= WebServerExample.workers) {
					/* too many threads, exit this one */
					return;
				} else {
					pool.addElement(this);
				}
			}
		}
	}

	void handleClient() throws IOException {
		InputStream is = new BufferedInputStream(s.getInputStream());
		PrintStream ps = new PrintStream(s.getOutputStream());
		/*
		 * we will only block in read for this many milliseconds before we fail
		 * with java.io.InterruptedIOException, at which point we will abandon
		 * the connection.
		 */
		s.setSoTimeout(WebServerExample.timeout);
		s.setTcpNoDelay(true);
		/* zero out the buffer from last time */
		for (int i = 0; i < BUF_SIZE; i++) {
			buf[i] = 0;
		}
		try {
			/*
			 * We only support HTTP GET/HEAD, and don't support any fancy HTTP
			 * options, so we're only interested really in the first line.
			 */
			int nread = 0, r = 0;

			outerloop: while (nread < BUF_SIZE) {
				r = is.read(buf, nread, BUF_SIZE - nread);
				if (r == -1) {
					/* EOF */
					return;
				}
				int i = nread;
				nread += r;
				for (; i < nread; i++) {
					if (buf[i] == (byte) '\n' || buf[i] == (byte) '\r') {
						/* read one line */
						break outerloop;
					}
				}
			}

			/* are we doing a GET or just a HEAD */
			boolean doingGet;
			/* beginning of file name */
			int index;
			if (buf[0] == (byte) 'G' && buf[1] == (byte) 'E'
					&& buf[2] == (byte) 'T' && buf[3] == (byte) ' ') {
				doingGet = true;
				index = 4;
			} else if (buf[0] == (byte) 'H' && buf[1] == (byte) 'E'
					&& buf[2] == (byte) 'A' && buf[3] == (byte) 'D'
					&& buf[4] == (byte) ' ') {
				doingGet = false;
				index = 5;
			} else {
				/* we don't support this method */
				ps.print("HTTP/1.0 " + HTTP_BAD_METHOD
						+ " unsupported method type: ");
				ps.write(buf, 0, 5);
				ps.write(EOL);
				ps.flush();
				s.close();
				return;
			}

			int i = 0;
			/*
			 * find the file name, from: GET /foo/bar.html HTTP/1.0 extract
			 * "/foo/bar.html"
			 */
			for (i = index; i < nread; i++) {
				if (buf[i] == (byte) ' ') {
					break;
				}
			}
			String fname = (new String(buf, 0, index, i - index)).replace('/',
					File.separatorChar);
			if (fname.startsWith(File.separator)) {
				fname = fname.substring(1);
			}
			File targ = new File(WebServerExample.root, fname);
			if (targ.isDirectory()) {
				File ind = new File(targ, "index.html");
				if (ind.exists()) {
					targ = ind;
				}
			}
			boolean OK = printHeaders(targ, ps);
			if (doingGet) {
				if (OK) {
					sendFile(targ, ps);
				} else {
					send404(targ, ps);
				}
			}
		} finally {
			s.close();
		}
	}

	boolean printHeaders(File targ, PrintStream ps) throws IOException {
		boolean ret = false;
		int rCode = 0;
		if (!targ.exists()) {
			rCode = HTTP_NOT_FOUND;
			ps.print("HTTP/1.0 " + HTTP_NOT_FOUND + " not found");
			ps.write(EOL);
			ret = false;
		} else {
			rCode = HTTP_OK;
			ps.print("HTTP/1.0 " + HTTP_OK + " OK");
			ps.write(EOL);
			ret = true;
		}
		log("From " + s.getInetAddress().getHostAddress() + ": GET "
				+ targ.getAbsolutePath() + "-->" + rCode);
		ps.print("Server: Simple java");
		ps.write(EOL);
		ps.print("Date: " + (new Date()));
		ps.write(EOL);
		if (ret) {
			if (!targ.isDirectory()) {
				ps.print("Content-length: " + targ.length());
				ps.write(EOL);
				ps.print("Last Modified: " + (new Date(targ.lastModified())));
				ps.write(EOL);
				String name = targ.getName();
				int ind = name.lastIndexOf('.');
				String ct = null;
				if (ind > 0) {
					ct = (String) map.get(name.substring(ind));
				}
				if (ct == null) {
					ct = "unknown/unknown";
				}
				ps.print("Content-type: " + ct);
				ps.write(EOL);
			} else {
				ps.print("Content-type: text/html");
				ps.write(EOL);
			}
		}
		return ret;
	}

	void send404(File targ, PrintStream ps) throws IOException {
		ps.write(EOL);
		ps.write(EOL);
		ps.println("Not Found\n\n" + "The requested resource was not found.\n");
	}

	void sendFile(File targ, PrintStream ps) throws IOException {
		InputStream is = null;
		ps.write(EOL);
		if (targ.isDirectory()) {
			listDirectory(targ, ps);
			return;
		} else {
			is = new FileInputStream(targ.getAbsolutePath());
		}

		try {
			int n;
			while ((n = is.read(buf)) > 0) {
				ps.write(buf, 0, n);
			}
		} finally {
			is.close();
		}
	}

	/* mapping of file extensions to content-types */
	static java.util.Hashtable map = new java.util.Hashtable();

	static {
		fillMap();
	}

	static void setSuffix(String k, String v) {
		map.put(k, v);
	}

	static void fillMap() {
		setSuffix("", "content/unknown");
		setSuffix(".uu", "application/octet-stream");
		setSuffix(".exe", "application/octet-stream");
		setSuffix(".ps", "application/postscript");
		setSuffix(".zip", "application/zip");
		setSuffix(".sh", "application/x-shar");
		setSuffix(".tar", "application/x-tar");
		setSuffix(".snd", "audio/basic");
		setSuffix(".au", "audio/basic");
		setSuffix(".wav", "audio/x-wav");
		setSuffix(".gif", "image/gif");
		setSuffix(".jpg", "image/jpeg");
		setSuffix(".jpeg", "image/jpeg");
		setSuffix(".htm", "text/html");
		setSuffix(".html", "text/html");
		setSuffix(".text", "text/plain");
		setSuffix(".c", "text/plain");
		setSuffix(".cc", "text/plain");
		setSuffix(".c++", "text/plain");
		setSuffix(".h", "text/plain");
		setSuffix(".pl", "text/plain");
		setSuffix(".txt", "text/plain");
		setSuffix(".java", "text/plain");
	}

	void listDirectory(File dir, PrintStream ps) throws IOException {
		ps.println("<TITLE>Directory listing</TITLE><P>\n");
		ps.println("<A HREF=\"..\">Parent Directory</A><BR>\n");
		String[] list = dir.list();
		for (int i = 0; list != null && i < list.length; i++) {
			File f = new File(dir, list[i]);
			if (f.isDirectory()) {
				ps.println("<A HREF=\"" + list[i] + "/\">" + list[i]
						+ "/</A><BR>");
			} else {
				ps.println("<A HREF=\"" + list[i] + "\">" + list[i] + "</A><BR");
			}
		}
		ps.println("<P><HR><BR><I>" + (new Date()) + "</I>");
	}

}

interface HttpConstants {
	/** 2XX: generally "OK" */
	public static final int HTTP_OK = 200;
	public static final int HTTP_CREATED = 201;
	public static final int HTTP_ACCEPTED = 202;
	public static final int HTTP_NOT_AUTHORITATIVE = 203;
	public static final int HTTP_NO_CONTENT = 204;
	public static final int HTTP_RESET = 205;
	public static final int HTTP_PARTIAL = 206;

	/** 3XX: relocation/redirect */
	public static final int HTTP_MULT_CHOICE = 300;
	public static final int HTTP_MOVED_PERM = 301;
	public static final int HTTP_MOVED_TEMP = 302;
	public static final int HTTP_SEE_OTHER = 303;
	public static final int HTTP_NOT_MODIFIED = 304;
	public static final int HTTP_USE_PROXY = 305;

	/** 4XX: client error */
	public static final int HTTP_BAD_REQUEST = 400;
	public static final int HTTP_UNAUTHORIZED = 401;
	public static final int HTTP_PAYMENT_REQUIRED = 402;
	public static final int HTTP_FORBIDDEN = 403;
	public static final int HTTP_NOT_FOUND = 404;
	public static final int HTTP_BAD_METHOD = 405;
	public static final int HTTP_NOT_ACCEPTABLE = 406;
	public static final int HTTP_PROXY_AUTH = 407;
	public static final int HTTP_CLIENT_TIMEOUT = 408;
	public static final int HTTP_CONFLICT = 409;
	public static final int HTTP_GONE = 410;
	public static final int HTTP_LENGTH_REQUIRED = 411;
	public static final int HTTP_PRECON_FAILED = 412;
	public static final int HTTP_ENTITY_TOO_LARGE = 413;
	public static final int HTTP_REQ_TOO_LONG = 414;
	public static final int HTTP_UNSUPPORTED_TYPE = 415;

	/** 5XX: server error */
	public static final int HTTP_SERVER_ERROR = 500;
	public static final int HTTP_INTERNAL_ERROR = 501;
	public static final int HTTP_BAD_GATEWAY = 502;
	public static final int HTTP_UNAVAILABLE = 503;
	public static final int HTTP_GATEWAY_TIMEOUT = 504;
	public static final int HTTP_VERSION = 505;
}
